<html>
<head>
    <title>Variables</title>
    <script type="text/javascript" src="protovis-d3.2.js"></script>
    <script type="text/javascript" src="../visualization/jquery/jquery-1.4.2.js"></script>
    <!-- 
       data.js defines a variable 'data', a rectangular array of text strings (some of which may represent numbers). 
         We expect the first row to contain column labels. 
       studentMCAS.js defines studentMCAS, a similar array containing student MCAS histories per subject.
         In this array numbers are already numbers and missing values are already nulls.
    -->
    <script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/data.js"></script>
    <script type="text/javascript" src="file://C:/Documents and Settings/dolsson/My Documents/My Dropbox/edu/districts/Gateway/Visualization for Todds spreadsheet/student MCAS.js"></script>

    <style type="text/css">
        .header-left { text-align: left }
        .padleft { padding-left: 10px }
        #variablesDiv { font-family: sans-serif; margin-top: 0.25in }
        .checkboxColumn { text-align: center;  }
    </style>
</head>

<body>
<div id="parallelCoordDiv">
<script type="text/javascript">

var vis;  // the root panel of our visualization

var wOverall = screen.availWidth * 0.9,  // be greedy with available screen space
    hOverall = screen.availHeight * 0.8;
$('#parallelCoordDiv').hide().width(wOverall).height(hOverall);  // size our host div, but hide it for now
var padding = { left: 30, right: 30, top: 30, bottom: 20 };
var w = wOverall - padding.left - padding.right,  // main visualization will be w x h pixels
    h = hOverall - padding.top - padding.bottom;

// The file data.js fed us variables in columns, labelled in the first row.
// Create an object for each variable.
// Keep the object list in the same order as the data columns.
var vars = data.shift().map( function(label, col) { return { label: label, position: col } } );
    // label - contents of the first row in the column
    // position - column index, from 0 (same as position in vars array)
    // uniqCount, hasEmpty, uniqNumCount, uniqTextCount, min, max, textSample, mostlyNum
    //      - see analyzeColumn()
    // type - see classifyVariable()
    // scale(val) - vertical scaling function, see mkScale()
    // visible(val) - visibility function, see mkVisibility()

// mcasVars too.
//var mcasVars = studentMCAS.shift().map( function(label, col) { return { label: label, position: col } } );

var displayCols = [];   // column indices of variables we're displaying at any given time
var filter = [];        // filters [not used yet]
var textSampleSize = 5; // number of text values to store as sample values

// Convert numberic strings to numbers. Convert empty strings to nulls.
// Special case: 
//    Treat strings of more than 5 digits with no decimal point as textual identifiers.
//    We encounter decimal ID codes with leading zeros that we need to keep.
data.forEach( function(row) { row.forEach( function(val, i, row) 
    { if (val == '') row[i] = null; else if (val.length < 6 && !isNaN(Number(val))) row[i] = Number(val) } ) } );

// Analyze and characterize the range of values in each column.
vars.forEach(analyzeColumn);
// Guess variable types: empty; identifier; grade; category; score (numeric).
vars.forEach(classifyVariable);
// Create vertical scale functions and visibility functions for each variable.
vars.forEach(mkScale);
vars.forEach(mkVisibility);

// Display variables, when the DOM is ready.
$(displayVariables);

function analyzeColumn(thisVar, i) {
    thisVar.uniqCount = 0;      // number of unique values, including null
    thisVar.hasEmpty = false;   // are there empty/null values?
    thisVar.uniqNumCount = 0;   // number of unique numeric values
    thisVar.uniqTextCount = 0;  // number of unique textual, nonnumeric values
    thisVar.min = null;         // minimum numeric value
    thisVar.max = null;         // maximum numeric value
    thisVar.textSample = [];    // sampling of textSampleSize text values
    var u = pv.uniq(data, function(row) { return row[i] } );
    u.forEach(analyzeUniq, thisVar);
    thisVar.mostlyNum = thisVar.uniqNumCount > thisVar.uniqTextCount * 4;
        // true if range of values is predominantly numeric
}

function analyzeUniq(val) {
    this.uniqCount++;
    if (val === null)
        this.hasEmpty = true;
    else if (typeof val == 'string') {
        this.uniqTextCount++;
        if (this.textSample.length < textSampleSize)
            this.textSample.push(val);
    }
    else {
        this.uniqNumCount++;
        if (this.min === null || val < this.min)
            this.min = val;
        if (this.max === null || val > this.max)
            this.max = val;
    }
}

function classifyVariable(thisVar) {
    // Really rough heuristics to classify variables...
    //   empty          - all nulls, no other values
    //   identifier     - occurence tags, especially per student
    //   SASID          - our favorite identifier
    //   category       - something to disaggregate by or count (default type)
    //   grade level    - one of our favorite categories
    //   score          - numeric metric
    //   MCAS scaled score - a favorite kind of score
    var SasidExp = /sasid/i;
    var IdExp = /lasid/i;
    var ScaledScoreExp = /scaled.score|mcas.ela|mcas.math|mcas.ste/i;
    var GradeExp = /grade/i;
    if (thisVar.uniqCount == 1 && thisVar.hasEmpty) thisVar.type = 'empty';
    else if (SasidExp.test(thisVar.label)) thisVar.type = 'SASID';
    else if (IdExp.test(thisVar.label)) thisVar.type = 'identifier';
    else if (GradeExp.test(thisVar.label)) thisVar.type = 'grade level';
    else if (thisVar.uniqCount < 4) thisVar.type = 'category';
    else if (thisVar.mostlyNum) {
        if (ScaledScoreExp.test(thisVar.label))
            thisVar.type = 'MCAS scaled score';
        else
            thisVar.type = 'score';
    }
    else if (thisVar.uniqCount > 0.8 * data.length) thisVar.type = 'identifier';
    else thisVar.type = 'category';
}

function mkScale(v) {
    // Make a vertical scaling function for this variable.
    // Create hybrid ranges for mixed text/numeric variables.
    // Assign text values positive or negative ordinals, with positives
    //   appearing above the numeric range and negatives appearing below.
    // [Later, allow ordinal or mixed domains to be sorted interactively.]
    switch (v.type) {
        case 'empty':
            v.scale = null;
            break;
        case 'SASID':
        case 'identifier':
        case 'category':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort()).split(0, h);
            break;
        case 'grade level':
            v.scale = pv.Scale.ordinal(pv.uniq(data, function(row) { return row[v.position] }).sort(numOrder)).split(0, h);
            break;
        case 'score':
            if (v.uniqCount == v.uniqNumCount)
                v.scale = pv.Scale.linear(v.min, v.max).range(0, h);
            else if (v.uniqTextCount == 0) {
                // Make a place on the scale for missing (null) values.
                var ls = pv.Scale.linear(v.min, v.max).range(0.1 * h, h);
                v.scale = function(val) {
                    if (val === null) return 0;
                    return ls(val);
                };
            }
            else {
                // Make places for text values and missing values.
                var u = pv.uniq(data, function(row) { return row[v.position] } );
                var tShare = (v.uniqText + v.hasEmpty) / v.uniqNumCount;  // text share of the axis
                var ts = pv.Scale.ordinal(u).split(0, tShare * h);
                var ls = pv.Scale.linear(v.min, v.max).range((1 - tShare) * h, h);
                v.scale = function(val) {
                    if (val === null)
                };
            }
            break;
        case 'MCAS scaled score':
            v.scale = pv.Scale.linear(200, 280).range(0, h);
            break;
    }
}

function mkVisibility(v) {
    v.visMin = null;
    v.visMax = null;
    v.visible = function(val) { 
        if (v.visMin === null)
            return true;
        var y = v.scale(val); 
        return y >= v.visMin && y <= v.visMax ;
    }
}

function displayVariables() {
    // Create a variable display with controls to
    //    select variables for display ('show')
    //    set the variable type
    //    filter data set by variable values
    // Show value ranges.
    $('#variablesDiv').append('<form><table id="variablesTable"><tr>' +
            '<th class="header-left">variable</th>' +
            '<th class="checkboxColumn">show</th>' +
            '<th>type</th>' +
            '<th>filter</th>' +
            '<th class="padleft header-left">range</th>' +
            '</tr></table></form>');
    vars.forEach(displayVar);
}

function displayVar(v) {
    // Build a display row for this variable.
    var tr = $('<tr><td class="varName">' + safe(v.label) + '</td></tr>');
    tr.append('<td class="checkboxColumn">' + '<input type="checkbox" name="column' + v.position + 'Show" onchange="showVar(this)" value="off">' + '</td>');
    tr.append('<td>' + varTypeSelectControl(v) + '</td>');
    tr.append('<td>' + varValueSelectControl(v) + '</td>');
    // Build a value range display.
    var text = '';
    if (v.mostlyNum)
        text += v.min + ' - ' + v.max;
    if (v.uniqTextCount) {
        if (text.length > 1) text += ', ';
        text += v.textSample.join(', ');
        if (v.uniqTextCount > v.textSample.length) text += ',...';
    }
    tr.append('<td class="padleft">' + text + '</td>');
    $('#variablesTable').append(tr);
}

function varTypeSelectControl(v) {
    // Build a variable type selection control for this variable.
    // Names are uniform: column<n>TypeSelect, where <n> is the column position.
    // Preselect whatever type we've determined so far.
    var html = '<select name="column' + v.position + 'TypeSelect" onchange="selectType(this)">';
    html += ['empty','identifier','SASID','grade level','category','score','MCAS scaled score'].map(
            function(t) { return '<option' + (t == v.type ? ' selected' : '') + '>' + t + '</option>' } );
    html += '</select>';
    return html;
}

function varValueSelectControl(v) {
    // Stub.
    var html = '<select multiple name="column' + v.position + 'ValueSelect" onchange="includeValues(this)">';
    html += '</select>';
    return '';
}

function colPosition(vars, label) {
    // Return the column index corresponding to the given label,
    // or null if none match.
    var v;
    for (v in vars)
        if (vars[v].label == label)
            return vars[v].position;
    return null;
}

function numOrder(a, b) { return a - b; }  // for numeric sorting

function safe(s) {
    // Return an HTML-safe string with character entities.
    s = s.replace(/&/g, '&amp;');
    s = s.replace(/</g, '&lt;');
    s = s.replace(/>/g, '&gt;');
    return s;
}

function showVar(checkbox) {
    // Called by a variable's 'show' checkbox when it changes.
    var nameExp = /column(\d+)Show/;  // parse the checkbox name for column position
    var match = nameExp.exec(checkbox.name);
    var column = Number(match[1]);  // column position
    if (checkbox.checked)
        displayCols.push(column);  // add to active display list
    else
        displayCols.splice(displayCols.indexOf(column), 1);  // remove from display list
    drawParallelCoords();  // redraw visualization
}

function rowVisible(row) {
    // Given a row from the data array, determine and return
    // whether it is 'visible' on all displayed variable axes.
    var dCol;  // index into displayCols
    var col;  // index into vars or row
    for (dCol = 0; dCol < displayCols.length; dCol++) {
        col = displayCols[dcol];
        if (!vars[col].visible(row[col]))
            return false;
    }
    return true;
}

function drawParallelCoords() {
    // Draw our visualization.
    var x = pv.Scale.ordinal(displayCols).split(0, w);
    vis = new pv.Panel()
        .canvas('parallelCoordDiv')
        .width(w)
        .height(h)
        .left(padding.left)
        .right(padding.right)
        .top(padding.top)
        .bottom(padding.bottom)
        .fillStyle('lightgray')
        ;
    vis.add(pv.Panel)  // display all occurrences (rows)
        .data(data)
        .add(pv.Line)
        .data(displayCols)
        .left(x)
        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
        .strokeStyle('gray')
        .add(pv.Dot)
        .fillStyle('aliceblue')
        .title( function(dCol, row) { return row[displayCols[dCol]] } )
        ;
//    var selected = vis.add(pv.Panel)  // highlight selected occurrences
//        .data(data)
//        .visible(rowVisible)
//        .add(pv.Line)
//        .data(displayCols)
//        .left(x)
//        .bottom( function(col, row) { return vars[col].scale(row[col]) } )
//        .strokeStyle('blue')
//        .add(pv.Dot)
//        .fillStyle('lightblue')
//        .title( function(col, row) { return row[col] } )
//        ;
    vis.add(pv.Label)  // axis labels
        .data(displayCols)
        .bottom(-5)
        .textBaseline('top')
        .left(x)
        .textAlign('center')
        .font("12px sans-serif")
        .text( function(col) { return vars[col].label } )
        .fillStyle('lightgray')
//        .events('all')
//        .event('click', deselect)
        ;
    vis.add(pv.Panel)  // axis value labels for categories and grades
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['category','grade level'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.domain() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(label) { return label ? label : '(none)' } )
        ;
    vis.add(pv.Panel)  // axis value labels for scores
        .data(displayCols)
        .left(x)
        .visible( function(col) { return ['score','MCAS scaled score'].indexOf(vars[col].type) >= 0 } )
        .add(pv.Label)
        .data( function(col) { return vars[col].scale.ticks() } )
        .bottom( function(val, col) { return vars[col].scale(val) } )
        .font("12px sans-serif")
        .text( function(val, col) { return vars[col].scale.tickFormat(val) } )
        ;
//    var selectZone = vis.add(pv.Panel)
//        .data(displayCols.map(function(col) { return {y:0, dy:h, col:col}; }))
//        .left(function(d) x(d.col) - 30)
//        .width(20)
//        .fillStyle("rgba(0,0,0,.001)")
//        .cursor("crosshair")
//        .event("mousedown", pv.Behavior.select())
//        .event("select", update)
//        .event("selectend", selectAll)
//        ;
    vis.render();

    // If our visualization div is hidden, bring it into view.
    if ($('#parallelCoordDiv').css('display') == 'none')
        $('#parallelCoordDiv').slideDown();
//    function update(d) {
//        vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
//        vars[d.col].visMax = Math.min(h, h - d.y);
//        vis.render();
//    }
//    function selectAll(d) {
//        console.log(d.dy);
//        if (d.dy != 0) {
//            vars[d.col].visMin = Math.max(0, h - d.y - d.dy);
//            vars[d.col].visMax = Math.min(h, h - d.y);
//            vis.render();
//        } else {
//            deselect(d.col);
//        }
//    }
//    function deselect(col) {
//        vars[col].minVis = null;
//        vars[col].maxVis = null;
//        vis.render();
//    }
}

</script>

</div>
<div id="variablesDiv">

</div>
</body>
</html>
